
package com.jf.cloud.auth.manager;

import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.symmetric.AES;
import com.alibaba.fastjson.JSON;
import com.jf.cloud.api.auth.bo.UserInfoInTokenBO;
import com.jf.cloud.api.auth.constant.SysTypeEnum;
import com.jf.cloud.api.auth.vo.TokenInfoVO;
import com.jf.cloud.auth.model.AuthAccount;
import com.jf.cloud.auth.service.AuthAccountService;
import com.jf.cloud.common.cache.constant.CacheNames;
import com.jf.cloud.common.cache.constant.OauthCacheNames;
import com.jf.cloud.common.response.ServerResponseEntity;
import com.jf.cloud.common.security.bo.TokenInfoBO;
import com.jf.cloud.common.util.Json;
import com.jf.cloud.common.util.PrincipalUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;

import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 * token管理 1. 登陆返回token 2. 刷新token 3. 清除用户过去token 4. 校验token
 *
 * @author zz
 * @date 2020/7/2
 */
@Component
@RefreshScope
public class TokenStore {

    private static final Logger logger = LoggerFactory.getLogger(TokenStore.class);

    private final RedisTemplate<Object, Object> redisTemplate;

    @Autowired
    private AuthAccountService authAccountService;

    public TokenStore(RedisTemplate<Object, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    /**
     * 以Sa-Token技术生成token，并返回token信息
     * @param userInfoInToken
     * @return
     */
    public TokenInfoBO storeAccessSaToken(UserInfoInTokenBO userInfoInToken) {
        logger.info("storeAccessSaToken-请求参数：{}" + Json.toJsonString(userInfoInToken));
        // token生成
        int timeoutSecond = getExpiresIn(userInfoInToken.getSysType());
        StpUtil.login(userInfoInToken.getUid(), timeoutSecond);
        String token = StpUtil.getTokenValue();
        // 用户信息存入缓存
        String keyName = OauthCacheNames.USER_INFO + token;
        redisTemplate.delete(keyName);
        redisTemplate.opsForValue().set(
                keyName,
                JSON.toJSONString(userInfoInToken),
                timeoutSecond,
                TimeUnit.SECONDS
        );
        // 数据封装返回(token不用加密)
        TokenInfoBO tokenInfoBO = new TokenInfoBO();
        tokenInfoBO.setUserInfoInToken(userInfoInToken);
        tokenInfoBO.setExpiresIn(timeoutSecond);
        tokenInfoBO.setAccessToken(token);
        tokenInfoBO.setRefreshToken(token);
        return tokenInfoBO;
    }

    /**
     * 计算过期时间（单位:秒）
     * @param sysType
     * @return
     */
    private int getExpiresIn(int sysType) {
        // 3600秒
        int expiresIn = 3600;
        // 普通用户token过期时间 1小时
        if (Objects.equals(sysType, SysTypeEnum.ORDINARY.value())) {
            expiresIn = expiresIn * 24 * 30;
        }
        // 系统管理员的token过期时间 2小时
        if (Objects.equals(sysType, SysTypeEnum.MULTISHOP.value()) || Objects.equals(sysType, SysTypeEnum.SUPPLIER.value()) || Objects.equals(sysType, SysTypeEnum.PLATFORM.value())) {
            expiresIn = expiresIn * 24 * 30;
        }
        return expiresIn;
    }

    /**
     * 根据accessToken 获取用户信息
     * @param accessToken accessToken
     * @param needDecrypt 是否需要解密
     * @return 用户信息
     */
    public ServerResponseEntity<UserInfoInTokenBO> getUserInfoByAccessToken(String accessToken, boolean needDecrypt) {
        if (StrUtil.isBlank(accessToken)) {
            return ServerResponseEntity.showFailMsg("accessToken is blank");
        }
        String keyName = OauthCacheNames.USER_INFO + accessToken;
        Object redisCache = redisTemplate.opsForValue().get(keyName);
        if (redisCache == null) {
            return ServerResponseEntity.showFailMsg("accessToken 已过期");
        }
        UserInfoInTokenBO userInfoInTokenBO = JSON.parseObject(redisCache.toString(), UserInfoInTokenBO.class);
        // 校验系统用户账号信息（是否存在 or 是否被禁用）
        if (SysTypeEnum.PLATFORM.value().equals(userInfoInTokenBO.getSysType())) {
            Long uid = userInfoInTokenBO.getUid();
            if (ObjectUtils.isEmpty(uid)) {
                return ServerResponseEntity.showFailMsg("accessToken 已过期");
            }
            AuthAccount user = authAccountService.getByUid(uid);
            if (user == null || user.getStatus() != 1) {
                return ServerResponseEntity.showFailMsg("accessToken 已过期");
            }
        }
        return ServerResponseEntity.success(userInfoInTokenBO);
    }

    /**
     * 刷新token，并返回新的token
     * @param refreshToken
     * @return
     */
    public ServerResponseEntity<TokenInfoBO> refreshToken(String refreshToken) {
        if (StrUtil.isBlank(refreshToken)) {
            return ServerResponseEntity.showFailMsg("refreshToken is blank");
        }
        // 删除旧token
        UserInfoInTokenBO userInfoInTokenBO = getUserInfoByAccessToken(refreshToken, false).getData();
        this.deleteCurrentToken(null, null, refreshToken);
        // 保存一份新的token
        TokenInfoBO tokenInfoBO = storeAccessSaToken(userInfoInTokenBO);
        return ServerResponseEntity.success(tokenInfoBO);
    }

    /**
     * 删除全部的token
     */
    public void deleteAllToken(String appId, Long uid) {
        // 删除用户缓存
        List<String> tokens = StpUtil.getTokenValueListByLoginId(uid);
        if (!CollectionUtils.isEmpty(tokens)) {
            List<Object> keyNames = new ArrayList<>();
            for (String token : tokens) {
                keyNames.add(OauthCacheNames.USER_INFO + token);
            }
            redisTemplate.delete(keyNames);
        }
        // 移除token
        StpUtil.logout(uid);
    }

    /**
     * 删除全部的token
     */
    public void deleteAllToken(Long userId, Integer sysType) {
        AuthAccount authAccount = authAccountService.getByUserIdAndType(userId, sysType);
        if (Objects.isNull(authAccount)) {
            return;
        }
        this.deleteAllToken(null, authAccount.getUid());
    }

    public TokenInfoVO storeAndGetVo(UserInfoInTokenBO userInfoInToken) {
        TokenInfoBO tokenInfoBO = storeAccessSaToken(userInfoInToken);
        TokenInfoVO tokenInfoVO = new TokenInfoVO();
        tokenInfoVO.setAccessToken(tokenInfoBO.getAccessToken());
        tokenInfoVO.setRefreshToken(tokenInfoBO.getRefreshToken());
        tokenInfoVO.setExpiresIn(tokenInfoBO.getExpiresIn());
        return tokenInfoVO;
    }

    public void deleteCurrentToken(String appId, Long uid, String accessToken) {
        // 删除用户缓存
        String keyName = OauthCacheNames.USER_INFO + accessToken;
        redisTemplate.delete(keyName);
        // 移除token
        StpUtil.logoutByTokenValue(accessToken);
    }

    public void updateUserInfoByUidAndAppId(Long uid, String appId, UserInfoInTokenBO userInfoInTokenBO) {
        if (userInfoInTokenBO == null) {
            return;
        }
        List<String> tokens = StpUtil.getTokenValueListByLoginId(uid);
        List<Object> keyNames = new ArrayList<>();
        for (String token : tokens) {
            keyNames.add(OauthCacheNames.USER_INFO + token);
        }
        // 更新userInfoToken
        redisTemplate.delete(keyNames);
        int timeoutSecond = getExpiresIn(userInfoInTokenBO.getSysType());
        String data = JSON.toJSONString(userInfoInTokenBO);
        for (Object keyName : keyNames) {
            redisTemplate.opsForValue().set(
                    keyName,
                    data,
                    timeoutSecond,
                    TimeUnit.SECONDS
            );
        }
    }
}
